﻿using System;
using ch11.binary;
using ch11.binary.type;

namespace ch11.interpreter
{
    public static partial class Instr
    {
        // hack!
        public static void Call(Vm vm, object args)
        {
            var f = vm.Funcs[(uint) args];
            CallFunc(vm, f);
        }

        public static void CallFunc(Vm vm, VmFunc f)
        {
            if (f.Func != null)
            {
                CallExternalFunc(vm, f);
            }
            else
            {
                CallInternalFunc(vm, f);
            }
        }

        private static void CallExternalFunc(Vm vm, VmFunc f)
        {
            var args = PopArgs(vm, f._type);
            var (results, err) = f.Func.Call(args);
            if (err != null)
            {
                Panic.Run(err);
            }

            PushResults(vm, f._type, results);
        }

        private static object[] PopArgs(Vm vm, FuncType ft)
        {
            var paramCount = ft.ParamTypes.Length;
            var args = new object[paramCount];
            for (var i = paramCount - 1; i >= 0; i--)
            {
                args[i] = WrapU64(ft.ParamTypes[i], vm.PopU64());
            }

            return args;
        }

        /*
         * operand stack:

+~~~~~~~~~~~~~~~+
|               |
+---------------+
|     stack     |
+---------------+
|     locals    |
+---------------+
|     params    |
+---------------+
|  ............ |
         */
        private static void CallInternalFunc(Vm vm, VmFunc f)
        {
            vm.EnterBlock(OpCodes.Call, ref f._type, ref f.Code.Expr);

            // alloc locals
            var localCount = (int) (f.Code.GetLocalCount());
            for (var i = 0; i < localCount; i++)
            {
                vm.PushU64(0);
            }
        }

        private static void CallIndirect(Vm vm, object args)
        {
            var typeIdx = (uint) args;
            var ft = vm.Module.TypeSec[typeIdx];

            var i = vm.PopU32();
            if (i >= vm.Table.Size())
            {
                Panic.Run(Errors.ErrUndefinedElem);
            }

            var f = vm.Table.GetElem(i);
            if (f.Type().GetSignature() != ft.GetSignature())
            {
                Panic.Run(Errors.ErrTypeMismatch);
            }

            // optimize internal func call

            if (f is VmFunc func)
            {
                if (func.Func == null && func.Vm.Equals(vm))
                {
                    CallInternalFunc(vm, func);
                    return;
                }
            }


            var fcArgs = PopArgs(vm, ft);
            var (results, err) = f.Call(fcArgs);
            if (err != null)
            {
                Panic.Run(err);
            }

            PushResults(vm, ft, results);
        }


        // hack!
        private static void CallAssertFunc(Vm vm, object args)
        {
            var idx = (uint) args;
            switch (vm.Module.ImportSec[idx].Name)
            {
                case "assert_true":
                    AssertEq(vm.OperandStack.PopBool(), true);
                    break;
                case "assert_false":
                    AssertEq(vm.OperandStack.PopBool(), false);
                    break;
                case "assert_eq_i32":
                    AssertEq(vm.OperandStack.PopU32(), vm.OperandStack.PopU32());
                    break;
                case "assert_eq_i64":
                    AssertEq(vm.OperandStack.PopU64(), vm.OperandStack.PopU64());
                    break;
                case "assert_eq_f32":
                    AssertEq(vm.OperandStack.PopF32(), vm.OperandStack.PopF32());
                    break;
                case "assert_eq_f64":
                    AssertEq(vm.OperandStack.PopF64(), vm.OperandStack.PopF64());
                    break;
                default:
                    Panic.Run("TODO");
                    break;
            }
        }

        private static void AssertEq(object a, object b)
        {
            if (!a.Equals(b))
            {
                Panic.Run("{0} != {1} ", a, b);
            }
        }

        private static void BrIf(Vm vm, object args)
        {
            if (vm.PopBool())
            {
                Br(vm, args);
            }
        }

        private static void BrTable(Vm vm, object args)
        {
            var brTableArgs = (BrTableArgs) args;
            var n = (int) (vm.PopU32());
            if (n < (brTableArgs.Labels.Length))
            {
                Br(vm, brTableArgs.Labels[n]);
            }
            else
            {
                Br(vm, brTableArgs.Default);
            }
        }

        private static void Return(Vm vm, object args)
        {
            var (_, labelIdx) = vm.ControlStack.TopCallFrame();
            Br(vm, (uint) (labelIdx));
        }

        private static void Unreachable(Vm vm, object args)
        {
            Panic.Run(Errors.ErrTrap);
        }

        private static void Nop(Vm vm, object args)
        {
            // do nothing
        }

        private static void Block(Vm vm, object args)
        {
            var blockArgs = (BlockArgs) args;
            var bt = vm.Module.GetBlockType(blockArgs.Bt);
            vm.EnterBlock(OpCodes.Block, ref bt, ref blockArgs.Instrs);
        }

        private static void Loop(Vm vm, object args)
        {
            var blockArgs = (BlockArgs) args;
            var bt = vm.Module.GetBlockType(blockArgs.Bt);
            vm.EnterBlock(OpCodes.Loop, ref bt, ref blockArgs.Instrs);
        }

        private static void If(Vm vm, object args)
        {
            var ifArgs = (IfArgs) args;
            var bt = vm.Module.GetBlockType(ifArgs.Bt);
            if (vm.PopBool())
            {
                vm.EnterBlock(OpCodes.If, ref bt, ref ifArgs.Instrs1);
            }
            else
            {
                vm.EnterBlock(OpCodes.If, ref bt, ref ifArgs.Instrs2);
            }
        }

        private static void Br(Vm vm, object args)
        {
            var labelIdx = (int) ((uint) args);
            for (var i = 0; i < labelIdx; i++)
            {
                vm.PopControlFrame();
            }

            var cf = vm.ControlStack.TopControlFrame();
            if (cf.Opcode != OpCodes.Loop)
            {
                vm.ExitBlock();
            }
            else
            {
                vm.ResetBlock(cf);
                cf.Pc = 0;
            }
        }

        private static void PushResults(Vm vm, FuncType ft, object[] results)
        {
            if ((ft.ResultTypes?.Length ?? 0) != (results?.Length ?? 0))
            {
                Panic.Run("TODO");
            }

            if (results == null) return;
            if (ft.ResultTypes == null) return;
            foreach (var result in results)
            {
                vm.PushU64(UnwrapU64(ft.ResultTypes[0], result));
            }
        }

        public static object WrapU64(ValType vt, ulong val)
        {
            switch (vt)
            {
                case ValType.ValTypeI32:
                    return (int) (val);
                case ValType.ValTypeI64:
                    return (long) (val);
                case ValType.ValTypeF32:
                    var u32Bit = BitConverter.GetBytes((uint) val);
                    return BitConverter.ToSingle(u32Bit);
                case ValType.ValTypeF64:
                    var u64Bit = BitConverter.GetBytes((uint) val);
                    return BitConverter.ToDouble(u64Bit);
                default:
                    // TODO
                    Panic.Run("unreachable");
                    return null;
            }
        }

        public static ulong UnwrapU64(ValType vt, object val)
        {
            switch (vt)
            {
                case ValType.ValTypeI32:
                    return (ulong) ((int) val);
                case ValType.ValTypeI64:
                    return (ulong) ((long) val);
                case ValType.ValTypeF32:
                    var f32Bits = BitConverter.GetBytes((float) val);
                    return BitConverter.ToUInt64(f32Bits);
                case ValType.ValTypeF64:
                    var f64Bits = BitConverter.GetBytes((double) val);
                    return BitConverter.ToUInt64(f64Bits);
                default:
                    Panic.Run("unreachable"); // TODO
                    return 0;
            }
        }
    }
}